-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
141882981-Add-question-type-to-content-loader-and-summary-display #36
141882981-Add-question-type-to-content-loader-and-summary-display #36
Conversation
Add new interfaces for `date` type questions (3 part d, m, y inputs). * `dmcontent/content_loader.py` * Extrapolate out assurance unformatting into new method * Add method to unformat dates * Add `_is_date` method on content to find out if a question is a date * `dmcontent/questions.py` * Add property `is_date` to question * Add `Question` class `Date` as interface for date questions * Add `DateSummary` `QuestionSummary` class to present date in given format * Make `date` type questions available in `QUESTION_TYPES` * Handle old date strings in date summary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like unformat_data
for a date question should be on the DateQuestion
itself. Similar to how we do for dynamic lists (https://github.com/alphagov/digitalmarketplace-content-loader/blob/master/dmcontent/questions.py#L369) and all question types (https://github.com/alphagov/digitalmarketplace-content-loader/blob/master/dmcontent/questions.py#L114).
However the reason this works (from a quick glance around one of the frontend apps) is because we are calling unformat_data
directly on the question in our views (https://github.com/alphagov/digitalmarketplace-supplier-frontend/blob/1bc8418226e15ba6a92f4cff7c25c23dbcf8ddf6/app/main/views/briefs.py#L202) rather than using the content section unformat_data
.
Maybe we should be trying to mimic the approach of get_data
(https://github.com/alphagov/digitalmarketplace-content-loader/blob/master/dmcontent/content_loader.py#L64) where when getting data for a section we run through every section and then every question letting each question then be responsible for getting it's own data. Doing it that way would feel more object orientated than the current solution. Ideally we wouldn't add a new 'if' statement every time unformatting data is different for a different type of question.
Note, the reason assurance appears to be an exception is because we do not have an AssuranceQuestion
, but maybe this could be restructured similarly to be on on question types that can have assurance rather than a content section.
What do you think (especially if I haven't spotted something or my suggestion isn't possible/sensible)?
dmcontent/questions.py
Outdated
@property | ||
def value(self): | ||
try: | ||
return datetime.strptime(self._value, '%Y-%m-%d').strftime('%A %-d %B %Y') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are formatting it into the common digital marketplace format we would ideally use the dmutils DATE_FORMAT
instead of duplicating
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a reason we we've been avoiding that. Has to do with having to add it as a dependency link in the setup.py as digitalmarketplace-content-loader
is a module. I've added it, we can chat on Friday.
return ContentQuestion(data) | ||
|
||
def test_date_is_formatted_into_user_friendly_format(self): | ||
question = self.question().summary({'example': '2016-02-18'}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth having a test for where we don't include 0 padding i.e. 2
?
Either we are saving the month input directly in which case we may see data like {'example': '2017-2-19'}
in our DB, and therefore we need to know our DateSummary is able to handle that or we only save data that includes 0 padding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, the test below now addresses this.
dmcontent/questions.py
Outdated
"""Retreive the fields from the POST data (form_data). | ||
|
||
The d, m, y should be in the post as 'questionName-day', questionName-month ... | ||
Extract them and format as 'YYYY-MM-DD'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess technically we can't gurantee it will be 'YYYY-MM-DD' as the user input could be anything such as 'YYYY-M-D'. This is a really anal comment though...
dmcontent/content_loader.py
Outdated
def unformat_data(self, data): | ||
|
||
"""Unpack assurance information to be used in a form |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do decide to stick with this solution then documentation for this function needs updating. Also is it worth adding a test to cover unformat_date
and maybe calling unformat_data
when passing a date field in?
dmcontent/questions.py
Outdated
try: | ||
return datetime.strptime(self._value, '%Y-%m-%d').strftime('%A %-d %B %Y') | ||
except ValueError: | ||
return self._value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be nice to include a very short comment explaining that we are falling back to original date format
8f78029
to
0463711
Compare
0463711
to
f687cd5
Compare
dmcontent/questions.py
Outdated
@@ -182,6 +185,10 @@ def inject_brief_questions_into_boolean_list_question(self, brief): | |||
def has_assurance(self): | |||
return True if self.get('assuranceApproach') else False | |||
|
|||
@property |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need this?
dmcontent/questions.py
Outdated
|
||
FIELDS = ('year', 'month', 'day') | ||
|
||
@property |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto here, is this now redundant?
dmcontent/questions.py
Outdated
parts = [] | ||
for key in self.FIELDS: | ||
identifier = '-'.join([self.id, key]) | ||
value = form_data.get(identifier, '').replace('-', '').strip() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should have a test for the stripping -
behaviour
tests/test_questions.py
Outdated
'example-day': '19', | ||
'example-month': '03', | ||
'example-year': '', | ||
}) == {'example': None} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we expect this to be None
rather than -03-19
?
dmcontent/questions.py
Outdated
|
||
return {self.id: '-'.join(parts) if any(parts) else None} | ||
|
||
def unformat_data(self, data): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have any tests for this method?
dmcontent/content_loader.py
Outdated
def unformat_data(self, data): | ||
|
||
"""Unpack assurance information to be used in a form |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doc block top line doesn't feel correct anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looks good.
Few comments regarding some potentially redundant things, old comments and missing test coverage. It will also need a version bump.
What is your view on using the dmutils dependancy? You seemed to imply there are some downsides to doing this?
digitalmarketplace-content-loader/tests/test_content_loader.py::TestReadYaml::test_loading_existant_file The above was failing on master, there's been a refactor recently but not sure what caused it tbh. Fixed mock to point at instance open that is used by the function being tested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking very good Ben.
Think I left just 3 minors now comments. One potential typo and one suggested comment improvement.
The only thing left is regarding the interface for unformat_data
which I preferred the old way and am not quite sure if there is a reason for us to do it otherwise. Maybe you could shed some light?
dmcontent/content_loader.py
Outdated
def unformat_data(self, data): | ||
"""Unpack assurance information to be used in a form | ||
"""Method to process form data, special assurance case or individual question level unformat. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might suggest something including something along the lines of into data to be used in a form
would be good to have in here as at the moment it reads like you are processing form data rather than processing some data that you can then pass to a form.
dmcontent/questions.py
Outdated
@@ -366,7 +369,7 @@ def get_data(self, form_data): | |||
|
|||
return {self.id: questions_data} | |||
|
|||
def unformat_data(self, data): | |||
def unformat_data(self, key, data): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we want to have two different interfaces for unformat_data
? It felt nice that unformat_data
had the same interface regardless of if you were calling it on a Section
or Question
. That pattern also mimics the get_data
usage.
tests/test_content_loader.py
Outdated
def test_loading_existant_file(self, mocked_open): | ||
assert read_yaml('anything.yml') == {'foo': 'bar'} | ||
|
||
@mock.patch.object(builtins, 'open', side_effect=IOError) | ||
@ mock.patch('dmcontent.content_loader.open', side_effect=IOError) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be a space in here?
I'm slightly confused about what we want Originally Your new examples take a different approach of taking a dict of data but only returning data for that question e.g.
Assuming consistency is desired, raises 2 questions for me:
|
I mean
We can't know what fields are relevant to a question before the data goes in so we can't only pass in the relevant fields to be unformatted.
It comes down to the whole mutable dict thing.
There are 2 possibilities here. Let's say the goal of our
or
The first mutates the given dictionary, the second creates a new one. The second (the way it's done now) creates a new dictionary containing all the data
The problem arises with the second one when we start looping over questions.
it's less valuable to define the new dictionary inside the method because you then lose the knowledge of which fields refer to a given question. So my way:
Avoids mutation, refers only to the given question, does not return data that may already have been unformatted.
with no side effects. |
Cool. I think I like your argument (thank's for taking the time to write it out clearly). It seems sensible to go with your suggestion but we should probably make @allait If you've got a minute, would it be possible just for a quick check on our plans for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a comment on the pricing unformat_data
, but overall this seems sensible and closer to how we delegate other things between section / multiquestion and questions.
It might be worth revisiting the DynamicList
implementation and updating it with the new approach (as it seems to look through the data for it's own key at the moment) and since it now should be called from the section.unformat_data
we can make this a breaking change if we need to and replace the DynamicList.unformat_data
calls in the apps with section.unformat_data
.
dmcontent/questions.py
Outdated
@@ -464,6 +467,9 @@ def get_question(self, field_name): | |||
if self.id == field_name or field_name in self.fields.values(): | |||
return self | |||
|
|||
def unformat_data(self, data): | |||
return self.get_data(data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this part is interesting. These 2 lines are confusing because unformat_data
and get_data
perform opposite operations, so they can't have the same implementation.
The reason we need this with the proposed implementation of ContentSection.unformat_data
is that it delegates based on question.get_question
and Pricing
returns itself whenever any of it's fields are passed in. Which means that if I understand this correctly this method will get called N times for a pricing question with N fields and each time the return value will overwrite all pricing fields in the section's unformatted data dictionary. Which is not a problem because this return value is always the same.
However, this seems to suggest that we might simplify this by passing in the original field key
to the quesiton.unformat_data(self, data, key)
- which then allows us to use the base class implementation and return {key: data[key]}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although the same can be said about get_data
and maybe having a similar interface between two methods makes sense.
On the other hand, they iterate over different things: get_data
iterates over questions and unformat_data
iterates over data keys.
@allait I believe the last 2 commits hsould address your comments. I've tried to clean up some of the code and make it clearer with comments what's going on. This has a version bump so do I need to update the changelog with the changes to unformat data on all questions? |
92b681e
to
5823b10
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good Ben!
One or two whitespace type comments and a question about data not existing but in principle this looks real good.
Just those tiny things to address and I know there is currently a failing Travis test to fix too.
After that it will just be the version bump. Technically we are changing the interface for DynamicList.unformat_data
so it would be a breaking change... however I'm not sure if our apps actually rely on it so we might not need to change any code. So I'm not sure if it should be major or minor...
dmcontent/content_loader.py
Outdated
@@ -11,7 +11,7 @@ | |||
from werkzeug.datastructures import ImmutableMultiDict | |||
|
|||
from .errors import ContentNotFoundError, QuestionNotFoundError | |||
from .questions import Question, ContentQuestion | |||
from .questions import Question, ContentQuestion, Date |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is Date
potentially an unused import?
dmcontent/questions.py
Outdated
"evidence-0": "Yes, I did." | ||
"nonDynamicListKey": 'other data' | ||
} | ||
"evidence-0": "Yes, I did." } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weird bit of whitespace in here
dmcontent/questions.py
Outdated
root, index = question.id.split('-') | ||
question_data = data[self.id][int(index)] | ||
if root in question_data: | ||
result.update({question.id: question_data.get(root)}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, this method reads fairly easily.
Out of interest, is there a particular reason to use .update
rather than result[question.id]
as before or is this just a personal preference thing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So one of the reasons I use update is to avoid the similar syntax when updating a list. Means that at a glace I can tell that result is a dict.
>>> x=[1, 2, 3, 4]
>>> y=2
>>> z=3
>>> x[y] = z
>>> x={1:2, 3:4}
>>> x[y]=z
# With the above x can be list or dict.
# With the below it can only be a dict
>>> x.update({y: z})
Another is that I like to use it like so especially in test fixtures:
def get_fake_brief(**kwargs):
data = {
'id':1,
'lastChangedDate': '00:00:00.00000',
'frameworkSlug': 'digital-outcomes-and-specialists'
...
}
data.update(kwargs)
return data
brief1 = get_fake_brief()
brief2 = get_fake_brief(id=2)
brief3 = get_fake_brief(id=3, frameworkSlug='digital-outcomes-and-specialists-2')
Finally in multiple updates it avoids a for loop:
my_nonsense_dict = {}
my_updates = {'foo': 'bar', 'baz': 'quux'}
for key in my_updates:
my_nonsense_dict[key] = my_updates[key]
my_nonsense_dict = {}
my_updates = {'foo': 'bar', 'baz': 'quux'}
my_nonsense_dict.update(my_updates)
So yeah it's kind of personal preference but founded on the fact that if I want to do the above things and keep my code consistent then I have to use .update
everywhere.
dmcontent/questions.py
Outdated
@@ -112,7 +115,7 @@ def get_error_messages(self, errors, question_descriptor_from="label"): | |||
return question_errors | |||
|
|||
def unformat_data(self, data): | |||
return data | |||
return {self.id: data[self.id]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this use .get
by default in case the key does not exist in data?
For some routes, we call unformat_data
on a question directly (https://github.com/alphagov/digitalmarketplace-supplier-frontend/blob/master/app/main/views/briefs.py#L202) and the data might not yet exist.
5823b10
to
bd77101
Compare
bd77101
to
c2f8b4d
Compare
OK, should be good to go @idavidmcdonald |
CHANGELOG.md
Outdated
|
||
### What changed | ||
|
||
New question type `Date` and non-backwards compatible change to `Question.unformat_data` which not returns only data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo "not" should be "now" I assume
There is a single typo that might be nice to fix but apart from that awesome work! |
9a2c104
to
c5dae54
Compare
Create handlers for processing and displaying date questions.
Date
class for handling date questions.DateSummary
class for handling viewing dates in summary tables etc.Date
handler inQUESTION_TYPES
to make it auto used by all date type questions.https://trello.com/c/G3J4fbtw/30-add-question-type-to-content-loader-and-summary-display